#pragma once

#include "curlwork/option.hpp"
#include "utility/function_np.hpp"

namespace curlwork {

template<typename T>
class operation
{
    typedef T object_type;

    typedef operation<object_type> this_type;

    object_type& object_;

    easy_handle_type handle_;

    delegate handler_;

    typedef utility::function_0p<void, object_type> complete_func;

    complete_func complete_;

    typedef utility::function_1p<void, object_type, error_type> fail_func;

    fail_func fail_;

    typedef utility::function_3p<std::size_t, object_type, const char *,
                                 std::size_t, std::size_t> io_func;

    io_func write_;
    io_func read_;
    io_func header_;

    typedef utility::function_2p<int, object_type,
                                 offset_type, int> seek_func;
    seek_func seek_;

    void free() throw()
    {
        if (handle_)
        {
            curl_easy_cleanup(handle_);
            handle_  = 0;
        }
    }

public:

    operation(object_type& object) throw()
        : object_(object)
        , handle_(curl_easy_init())
        , handler_(this, &this_type::handle_complete)
        , complete_()
        , fail_()
        , write_()
        , read_()
    {   }

    ~operation()
    {
        free();
    }

    const easy_handle_type& handle() const throw()
    {
        return handle_;
    }

    void reset()
    {
        curl_easy_reset(handle_);

        complete_.clear();
        fail_.clear();
        write_.clear();
        read_.clear();
    }

    void set(const url& u)
    {
        set(option(CURLOPT_NOSIGNAL, 1));

        u.apply(*this);
    }

    template<typename F>
    void set(const F& mod)
    {
        mod.apply(*this);
    }

    struct complete
    {
        complete_func func_;

        complete(const complete_func& func)
            : func_(func)
        {   }

        void apply(operation& op) const
        {
            op.complete_ = func_;

            op.set(binder(&op.handler_));
        }
    };

    struct fail
    {
        fail_func func_;

        fail(const fail_func& func)
            : func_(func)
        {   }

        void apply(operation& op) const
        {
            op.set(option(CURLOPT_FAILONERROR, 1));

            op.fail_ = func_;
        }
    };

    struct write
    {
        io_func func_;

        write(const io_func& func)
            : func_(func)
        {   }

        void apply(operation& op) const
        {
            error_type e = curl_easy_setopt(op.handle_,
                                            CURLOPT_WRITEFUNCTION,
                                            &this_type::handle_write);
            if (e != no_error)
                throw std::runtime_error(description(e));

            e = curl_easy_setopt(op.handle_, CURLOPT_WRITEDATA, &op);
            if (e != no_error)
                throw std::runtime_error(description(e));

            op.write_ = func_;
        }
    };

    struct read
    {
        io_func func_;

        read(const io_func& func)
            : func_(func)
        {   }

        void apply(operation& op) const
        {
            error_type e = curl_easy_setopt(op.handle_,
                                            CURLOPT_READFUNCTION,
                                            &this_type::handle_read);
            if (e != no_error)
                throw std::runtime_error(description(e));

            e = curl_easy_setopt(op.handle_, CURLOPT_READDATA, &op);
            if (e != no_error)
                throw std::runtime_error(description(e));

            op.read_ = func_;
        }
    };

    struct header
    {
        io_func func_;

        header(const io_func& func)
            : func_(func)
        {   }

        void apply(operation& op) const
        {
            error_type e = curl_easy_setopt(op.handle_,
                                            CURLOPT_HEADERFUNCTION,
                                            &this_type::handle_header);
            if (e != no_error)
                throw std::runtime_error(description(e));

            e = curl_easy_setopt(op.handle_, CURLOPT_WRITEHEADER, &op);
            if (e != no_error)
                throw std::runtime_error(description(e));

            op.header_ = func_;
        }
    };

    struct seek
    {
        seek_func func_;

        seek(const seek_func& func)
            : func_(func)
        {   }

        void apply(operation& op) const
        {
            error_type e = curl_easy_setopt(op.handle_,
                                            CURLOPT_SEEKFUNCTION,
                                            &this_type::handle_seek);
            if (e != no_error)
                throw std::runtime_error(description(e));

            e = curl_easy_setopt(op.handle_, CURLOPT_SEEKDATA, &op);
            if (e != no_error)
                throw std::runtime_error(description(e));

            op.seek_ = func_;
        }
    };

    void pause(int mask) const
    {
        error_type e = curl_easy_pause(handle_, mask);
        if (e != no_error)
            throw std::runtime_error(description(e));
    }

    void resume() const
    {
        error_type e = curl_easy_pause(handle_, CURLPAUSE_CONT);
        if (e != no_error)
            throw std::runtime_error(description(e));
    }

private:

    static inline std::size_t handle_write(const char *buffer,
                                           std::size_t size,
                                           std::size_t nmemb, void *obj)
    {
        this_type *op = reinterpret_cast<this_type*>(obj);
        return op->do_write(buffer, size, nmemb);
    }

    static inline std::size_t handle_read(const char *buffer,
                                          std::size_t size,
                                          std::size_t nmemb, void *obj)
    {
        this_type *op = reinterpret_cast<this_type*>(obj);
        return op->do_read(buffer, size, nmemb);
    }

    static inline std::size_t handle_header(const char *buffer,
                                            std::size_t size,
                                            std::size_t nmemb, void *obj)
    {
        this_type *op = reinterpret_cast<this_type*>(obj);
        return op->do_header(buffer, size, nmemb);
    }

    static inline int handle_seek(void *obj,
                                  offset_type offset,
                                  int origin)
    {
        this_type *op = reinterpret_cast<this_type*>(obj);
        return op->do_seek(offset, origin);
    }

    static inline void handle_complete(void *obj, error_type error)
    {
        this_type *op = reinterpret_cast<this_type*>(obj);

        if (error != no_error)
            op->do_fail(error);
        else
            op->do_complete();
    }

    std::size_t do_write(const char *buffer,
                         std::size_t size, std::size_t nmemb)
    {
        if (write_.empty())
            throw std::runtime_error("do_write");

        return write_(object_, buffer, size, nmemb);
    }

    std::size_t do_read(char *buffer,
                        std::size_t size, std::size_t nmemb)
    {
        if (read_.empty())
            throw std::runtime_error("do_read");

        return read_(object_, buffer, size, nmemb);
    }

    std::size_t do_header(const char *buffer,
                          std::size_t size, std::size_t nmemb)
    {
        if (header_.empty())
            throw std::runtime_error("do_header");

        return header_(object_, buffer, size, nmemb);
    }

    int do_seek(offset_type offset, int origin)
    {
        if (seek_.empty())
            throw std::runtime_error("do_seek");

        return seek_(offset, origin);
    }

    void do_complete()
    {
        if (!complete_.empty())
            complete_(object_);
    }

    void do_fail(error_type error)
    {
        if (!fail_.empty())
            fail_(object_, error);
    }
};

} // namespace curlwork
